# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, simpledialog
import os
import sys
import time
import re
import json
import math
import random
import threading
from typing import Dict, List, Tuple, Optional, Any
from dataclasses import dataclass
import psutil

# RflySim3D / UE4 Control API (Assuming these are in Python path or same directory)
try:
    import UE4CtrlAPI
    import UEMapServe
except ImportError:
    print("错误：UE4CtrlAPI.py 或 UEMapServe.py 未找到。请确保它们在PYTHONPATH中或与此脚本在同一目录。")
    # Fallback for UI testing without actual RflySim connection
    class MockUE4CtrlAPI:
        def sendUE4Cmd(self, cmd): print(f"Mock UE4Cmd: {cmd}")
        def sendUE4Pos(self, vehicleID, vehicleType, motorParam, position, rotation): print(f"Mock UE4Pos: ID={vehicleID}, Type={vehicleType}, Pos={position}, Rot={rotation}")
        def sendUE4PosScale(self, vehicleID, vehicleType, motorParam, position, rotation, scale): print(f"Mock UE4PosScale: ID={vehicleID}, Type={vehicleType}, Pos={position}, Rot={rotation}, Scale={scale}")

    class MockUEMapServe:
        def __init__(self, map_name="Grasslands"): self.map_name = map_name; print(f"Mock UEMapServe initialized for map: {map_name}")
        def getTerrainAltData(self, x, y): print(f"Mock getTerrainAltData for x={x}, y={y}"); return -8.0 # Default Z

    UE4CtrlAPI = MockUE4CtrlAPI
    UEMapServe = MockUEMapServe
    print("警告: UE4CtrlAPI 或 UEMapServe 未成功导入，将使用模拟对象。实际的RflySim3D交互将不可用。")


# --- Configuration Constants (from original scripts) ---
MODEL_TYPES = {
    "1": {"name": "无人机", "classid": 3},
    "2": {"name": "复合翼无人机", "classid": 6},
    "4": {"name": "汽车载具", "classid": 50},
    "5": {"name": "光球", "classid": 60},
    "6": {"name": "穿环模型", "classid": 150},
    "7": {"name": "巨球", "classid": 152}
}
CHARACTERS = {
    "1": {"name": "人物1", "walking_id": 3, "standing_id": 5},
    "2": {"name": "人物2", "walking_id": 7, "standing_id": 8},
    "3": {"name": "人物3", "walking_id": 10, "standing_id": 11},
    "4": {"name": "人物4", "walking_id": 13, "standing_id": 14}
}
CLICK_LOG_PATH = r"D:/PX4PSP/RflySim3D/ClickLog.txt" # Make sure this path is correct
ID_COUNTER_FILE = 'model_id_counter.txt'
DEFAULT_MAP_NAME = "Grasslands"

WEATHER_TYPES = {
    0: "晴天",
    1: "少云",
    2: "多云",
    3: "阴",
    4: "小雨",
    5: "雨",
    6: "暴雨",
    7: "小雪",
    8: "雪",
    9: "暴雪",
    10: "雾"
}

# --- Data Classes (from redeploy_models.py and character_motion.py) ---
class ModelInfo:
    def __init__(self, model_id: int, model_type: int, position: List[float], angles: Optional[List[float]] = None):
        self.model_id = model_id
        self.model_type = model_type
        self.position = position
        self.angles = angles or [0.0, 0.0, 0.0]

class CharacterInfo(ModelInfo):
    def __init__(self, model_id: int, name: str, walking_type: int, standing_type: int,
                 position: List[float], patrol_points: List[List[float]], motion_logic: Dict,
                 angles: Optional[List[float]] = None): # Added angles for consistency
        super().__init__(model_id, walking_type, position, angles)
        self.name = name
        self.walking_type = walking_type
        self.standing_type = standing_type
        self.patrol_points = patrol_points
        self.motion_logic = motion_logic

@dataclass
class MotionState:
    current_pos: List[float]
    current_point_index: int
    state: str
    state_start_time: float
    current_vehicle_type: int
    current_yaw: float

# --- Core Logic (Adapted from all four scripts) ---

# ID Management
def get_next_id(prefix_filter: str = 'MODEL:') -> int:
    if prefix_filter == 'MODEL:':
        start_id, max_id = 8000, 9999
    elif prefix_filter == 'CHARACTER:':
        start_id, max_id = 6000, 7999
    else:
        start_id, max_id = 6000, 9999
    used_ids = set()
    if os.path.exists(ID_COUNTER_FILE):
        with open(ID_COUNTER_FILE, 'r') as f:
            for line in f:
                try:
                    if line.startswith(prefix_filter):
                        parts = line.split('|')
                        if parts:
                            id_part = parts[0].split(':')
                            if len(id_part) > 1:
                                used_ids.add(int(id_part[1]))
                except ValueError:
                    pass
    next_id = start_id
    while next_id <= max_id:
        if next_id not in used_ids:
            return next_id
        next_id += 1
    raise Exception(f"ID for {prefix_filter} has reached the limit of {max_id}.")

def update_id_counter_header(map_name, weather, time_val):
    header = f"MapName:{map_name}, Weather:{weather}, Time:{time_val}\n"
    lines = []
    if os.path.exists(ID_COUNTER_FILE):
        with open(ID_COUNTER_FILE, 'r') as f:
            lines = f.readlines()
    # 判断首行是否已有header
    if lines and lines[0].startswith('MapName:'):
        lines[0] = header
    else:
        lines = [header] + lines
    with open(ID_COUNTER_FILE, 'w') as f:
        f.writelines(lines)

def record_to_id_counter_with_header(record_line: str, map_name, weather, time_val):
    update_id_counter_header(map_name, weather, time_val)
    with open(ID_COUNTER_FILE, 'a') as f:
        f.write(f"{record_line}\n")
        f.flush()
        os.fsync(f.fileno())
    time.sleep(0.01)
    if os.path.exists(ID_COUNTER_FILE):
        with open(ID_COUNTER_FILE, 'r') as f:
            lines = f.readlines()
            for last_line in reversed(lines):
                last_line = last_line.strip()
                if last_line and not last_line.startswith('MapName:'):
                    break
            if record_line.strip() != last_line.strip():
                raise Exception("记录验证失败：写入的内容与验证读取的内容不匹配")

def reset_id_counter_file():
    try:
        if os.path.exists(ID_COUNTER_FILE):
            os.remove(ID_COUNTER_FILE)
        # Create an empty file
        with open(ID_COUNTER_FILE, 'w') as f:
            pass
        return "模型ID计数器已成功重置。"
    except Exception as e:
        return f"重置ID计数器失败: {str(e)}"

# ClickLog Interaction
def get_last_click_position_from_log() -> Optional[List[float]]:
    if not os.path.exists(CLICK_LOG_PATH):
        return f"错误：找不到ClickLog文件: {CLICK_LOG_PATH}", None
    try:
        with open(CLICK_LOG_PATH, 'r') as f:
            lines = f.readlines()
        if not lines:
            return "ClickLog文件为空。", None
        
        if len(lines) == 1 and "Unit:m" in lines[0]:
             return "请在RflySim3D环境中点击位置以获取坐标。", None

        last_line = lines[-1].strip()
        pattern = r'Click Point: \[(S_Scanner_Click_Location_Regular|S_Scanner_Click_Location): \[(?P<x>[-\d\.]+), (?P<y>[-\d\.]+), (?P<z>[-\d\.]+)\]'
        # More robust pattern to catch different formats like "Click Point: [S_Scanner_Click_Location: [-7.06, -11.06, -8.13]]"
        # or "Click Point: [[-7.06, -11.06, -8.13]]"
        
        try:
            match_simple = re.search(r'Click Point: \[(?P<x>[-\d\.]+), (?P<y>[-\d\.]+), (?P<z>[-\d\.]+)\]', last_line)
            match_scanner = re.search(r'Click Point: \[S_Scanner_Click_Location(?:_Regular)?: \[(?P<x>[-\d\.]+), (?P<y>[-\d\.]+), (?P<z>[-\d\.]+)\]\]', last_line)
        except re.error as e:
            return f"正则表达式错误: {e}", None

        match = match_scanner if match_scanner else match_simple

        if match:
            x, y, z = map(float, match.groupdict().values())
            return None, [x, y, z] # No error, position
        else:
            return f"未在ClickLog最后一行检测到有效点击位置: '{last_line}'", None
    except Exception as e:
        return f"读取ClickLog时发生错误: {str(e)}", None

# Model Generation (from model_generator.py)
def create_static_model(model_classid: int, position: List[float], ue_api: Any, app=None) -> Tuple[Optional[str], Optional[int]]:
    try:
        model_id = get_next_id(prefix_filter='MODEL:')
        vehicle_type = model_classid
        angles = [0.00, 0.00, -0.21]
        ue_api.sendUE4Pos(model_id, vehicle_type, 0, position, angles)
        # 获取当前场景信息
        map_name = app.map_cmd_names[app.map_combo.current()] if app else 'Grasslands'
        weather = WEATHER_TYPES[app.weather_combo.current()] if app else '晴天'
        time_val = app.weather_time_var.get() if app else '1200'
        record_line = f"MODEL:{model_id}|TYPE:{vehicle_type}|POS:{position[0]:.2f},{position[1]:.2f},{position[2]:.2f}|ANGLE:{angles[0]:.2f},{angles[1]:.2f},{angles[2]:.2f}"
        record_to_id_counter_with_header(record_line, map_name, weather, time_val)
        return f"成功生成模型 ID: {model_id}, 类型: {vehicle_type}, 位置: {position}, 角度: {angles}", model_id
    except Exception as e:
        return f"生成静态模型时发生错误: {str(e)}", None

# Person Generation (Simplified from generate_person.py)
def generate_random_patrol_points(num_points: int, initial_point: List[float], radius: float = 10.0, map_server_api: Any = None) -> List[List[float]]:
    points = [initial_point]
    # Note: get_ground_z was part of generate_person.py, requires map_server
    # For now, we'll assume flat ground or use initial_point's Z
    def get_z(x,y):
        if map_server_api:
            # This was get_ground_z in generate_person.py which used UEMapServe
            # Make sure map_server_api is an instance of UEMapServe
            z_terrain = map_server_api.getTerrainAltData(x,y)
            return z_terrain if z_terrain is not None else initial_point[2] # fallback to initial Z
        return initial_point[2]


    for _ in range(num_points - 1):
        angle = random.uniform(0, 2 * math.pi)
        r = random.uniform(0, radius)
        x = initial_point[0] + r * math.cos(angle)
        y = initial_point[1] + r * math.sin(angle)
        z = get_z(x,y)
        points.append([round(x,2), round(y,2), round(z,2)])
    return points

def create_person_model(char_details: Dict, position: List[float], patrol_points: List[List[float]], ue_api: Any, app=None) -> Tuple[Optional[str], Optional[int], Dict]:
    try:
        person_id = get_next_id(prefix_filter='CHARACTER:')
        walking_type_val = char_details['walking_id'] * 100000 + 30
        standing_type_val = char_details['standing_id'] * 100000 + 30
        initial_angles = [0.0, 0.0, 0.0]
        ue_api.sendUE4Pos(person_id, walking_type_val, 0, position, initial_angles)
        motion_logic = {
            "type": "patrol",
            "speed": 1.2,
            "turn_threshold": 0.3,
            "showcase_duration": 2.0,
            "turn_speed": 6.0
        }
        points_str = ';'.join([f"{p[0]:.2f},{p[1]:.2f},{p[2]:.2f}" for p in patrol_points])
        map_name = app.map_cmd_names[app.map_combo.current()] if app else 'Grasslands'
        weather = WEATHER_TYPES[app.weather_combo.current()] if app else '晴天'
        time_val = app.weather_time_var.get() if app else '1200'
        record_line = (f"CHARACTER:{person_id}|NAME:{char_details['name']}|"
                      f"WALKING_TYPE:{walking_type_val}|STANDING_TYPE:{standing_type_val}|"
                      f"POS:{position[0]:.2f},{position[1]:.2f},{position[2]:.2f}|"
                      f"PATROL_POINTS:{points_str}|MOTION_LOGIC:{json.dumps(motion_logic)}")
        record_to_id_counter_with_header(record_line, map_name, weather, time_val)
        return f"成功生成人物 {char_details['name']} ID: {person_id} 在 {position}.", person_id, motion_logic
    except Exception as e:
        return f"生成人物模型时发生错误: {str(e)}", None, None


# Character Motion Control (from character_motion.py)
class SmoothTurnController:
    def __init__(self, turn_speed: float):
        self.target_yaw_rad = 0.0
        self.current_yaw_rad = 0.0
        self.turn_speed_rad_per_s = math.radians(turn_speed * 8) # Heuristic, adjust if needed. Original was unitless.

    def update(self, target_rad: float, delta_time_s: float) -> float:
        # Normalize angles to be between -pi and pi for smallest difference
        current_yaw_norm = math.atan2(math.sin(self.current_yaw_rad), math.cos(self.current_yaw_rad))
        target_yaw_norm = math.atan2(math.sin(target_rad), math.cos(target_rad))

        angle_diff_rad = target_yaw_norm - current_yaw_norm
        # Ensure angle_diff is the shortest path
        if angle_diff_rad > math.pi:
            angle_diff_rad -= 2 * math.pi
        if angle_diff_rad < -math.pi:
            angle_diff_rad += 2 * math.pi
        
        # Max turn this frame
        max_turn_rad = self.turn_speed_rad_per_s * delta_time_s
        
        # Actual turn
        turn_amount_rad = max(min(angle_diff_rad, max_turn_rad), -max_turn_rad)
        
        self.current_yaw_rad += turn_amount_rad
        # Normalize current_yaw_rad to be between 0 and 2*pi (or -pi to pi, consistently)
        self.current_yaw_rad = self.current_yaw_rad % (2 * math.pi)
        return self.current_yaw_rad

# 新增：全局人物运动统一调度器
class CharacterMotionManager:
    def __init__(self, app_logger):
        self.characters: List[CharacterInfo] = []
        self.states: Dict[int, MotionState] = {}
        self.yaw_controllers: Dict[int, SmoothTurnController] = {}
        self.ue = UE4CtrlAPI.UE4CtrlAPI()
        self.map_server = UEMapServe.UEMapServe(DEFAULT_MAP_NAME)
        self.logger = app_logger
        self.running = False
        self.sim_interval = 1/20.0  # 20Hz
        self.timer = None

    def add_character(self, char_info: CharacterInfo):
        self.characters.append(char_info)
        self.states[char_info.model_id] = MotionState(
            current_pos=list(char_info.position),
            current_point_index=0,
            state="moving",
            state_start_time=time.time(),
            current_vehicle_type=char_info.walking_type,
            current_yaw=0.0
        )
        self.yaw_controllers[char_info.model_id] = SmoothTurnController(
            turn_speed=char_info.motion_logic.get('turn_speed', 2.0)
        )
        self.logger(f"已添加人物 {char_info.name} (ID: {char_info.model_id}) 到统一调度器")

    def remove_all(self):
        self.characters.clear()
        self.states.clear()
        self.yaw_controllers.clear()
        self.logger("已清空所有人物运动状态")

    def update_all(self):
        now = time.time()
        for char in self.characters:
            state = self.states[char.model_id]
            logic = char.motion_logic
            speed = logic.get('speed', 1.5)
            turn_threshold = logic.get('turn_threshold', 0.3)
            showcase_duration = logic.get('showcase_duration', 2.0)
            patrol_points = char.patrol_points
            if not patrol_points:
                continue
            target_idx = (state.current_point_index + 1) % len(patrol_points)
            target_point = patrol_points[target_idx]
            if state.state == "moving":
                dx = target_point[0] - state.current_pos[0]
                dy = target_point[1] - state.current_pos[1]
                distance = math.sqrt(dx*dx + dy*dy)
                if distance < turn_threshold:
                    state.state = "showcasing"
                    state.state_start_time = now
                    state.current_vehicle_type = char.standing_type
                    state.current_point_index = target_idx
                else:
                    move_x = (dx / distance) * speed * self.sim_interval
                    move_y = (dy / distance) * speed * self.sim_interval
                    state.current_pos[0] += move_x
                    state.current_pos[1] += move_y
                    state.current_pos[2] = self.map_server.getTerrainAltData(state.current_pos[0], state.current_pos[1])
                    target_yaw_rad = math.atan2(dy, dx)
                    state.current_yaw = self.yaw_controllers[char.model_id].update(target_yaw_rad, self.sim_interval)
            elif state.state == "showcasing":
                if now - state.state_start_time >= showcase_duration:
                    state.state = "turning"
                    state.state_start_time = now
                    state.current_vehicle_type = char.walking_type
            elif state.state == "turning":
                next_target_idx = (state.current_point_index + 1) % len(patrol_points)
                next_target_point = patrol_points[next_target_idx]
                dx = next_target_point[0] - state.current_pos[0]
                dy = next_target_point[1] - state.current_pos[1]
                target_yaw_rad = math.atan2(dy, dx)
                state.current_yaw = self.yaw_controllers[char.model_id].update(target_yaw_rad, self.sim_interval)
                angle_diff_to_target = abs(target_yaw_rad - state.current_yaw)
                if angle_diff_to_target > math.pi:
                    angle_diff_to_target = 2 * math.pi - angle_diff_to_target
                if angle_diff_to_target < math.radians(5.0):
                    state.state = "moving"
            # 发送位置
            try:
                self.ue.sendUE4PosScale(
                    char.model_id,
                    state.current_vehicle_type,
                    0,
                    state.current_pos,
                    [0, 0, state.current_yaw],
                    [1, 1, 1]
                )
            except Exception as e:
                self.logger(f"人物 {char.name} (ID: {char.model_id}) 位置发送失败: {str(e)}")

    def loop(self):
        if not self.running:
            return
        self.update_all()
        self.timer = threading.Timer(self.sim_interval, self.loop)
        self.timer.start()

    def start(self):
        if not self.running:
            self.running = True
            self.loop()
            self.logger("统一人物运动调度器已启动")

    def stop(self):
        self.running = False
        if self.timer:
            self.timer.cancel()
        self.logger("统一人物运动调度器已停止")

# Redeploy Logic (from redeploy_models.py)
def parse_model_line(line: str) -> Optional[ModelInfo]:
    try:
        if not line.startswith('MODEL:'): return None
        parts = line.strip().split('|')
        model_id = int(parts[0].split(':')[1])
        model_type = int(parts[1].split(':')[1])
        pos_str = parts[2].split(':')[1]
        position = [float(x) for x in pos_str.split(',')]
        angles = [0.0,0.0,0.0]
        if len(parts) > 3 and parts[3].startswith('ANGLE:'):
            angle_str = parts[3].split(':')[1]
            angles = [float(x) for x in angle_str.split(',')]
        return ModelInfo(model_id, model_type, position, angles)
    except Exception: return None

def parse_character_line(line: str) -> Optional[CharacterInfo]:
    """解析人物记录行"""
    try:
        if not line.startswith('CHARACTER:'):
            return None
            
        parts = line.strip().split('|')
        if len(parts) < 7:  # 确保有足够的字段
            print(f"警告：人物记录格式不完整: {line}")
            return None
            
        # 解析基本信息
        model_id = int(parts[0].split(':')[1])
        name = parts[1].split(':')[1]
        walking_type = int(parts[2].split(':')[1])
        standing_type = int(parts[3].split(':')[1])
        
        # 解析位置
        pos_str = parts[4].split(':')[1]
        position = [float(x) for x in pos_str.split(',')]
        
        # 解析巡逻点和运动逻辑
        patrol_points = []
        motion_logic = {}
        
        for part in parts[5:]:
            if part.startswith('PATROL_POINTS:'):
                points_str = part.split('PATROL_POINTS:')[1]
                patrol_points = [[float(x) for x in pt.split(',')] for pt in points_str.split(';')]
            elif part.startswith('MOTION_LOGIC:'):
                try:
                    motion_logic = json.loads(part.split('MOTION_LOGIC:')[1])
                except json.JSONDecodeError as e:
                    print(f"警告：运动逻辑JSON解析失败: {e}")
                    motion_logic = {
                        "type": "patrol",
                        "speed": 1.5,
                        "turn_threshold": 0.3,
                        "showcase_duration": 2.0,
                        "turn_speed": 3.0
                    }
        
        # 验证必要数据
        if not patrol_points:
            print(f"警告：人物 {name} (ID: {model_id}) 没有巡逻点")
            return None
            
        if not motion_logic:
            print(f"警告：人物 {name} (ID: {model_id}) 没有运动逻辑，使用默认值")
            motion_logic = {
                "type": "patrol",
                "speed": 1.5,
                "turn_threshold": 0.3,
                "showcase_duration": 2.0,
                "turn_speed": 3.0
            }
        
        return CharacterInfo(
            model_id=model_id,
            name=name,
            walking_type=walking_type,
            standing_type=standing_type,
            position=position,
            patrol_points=patrol_points,
            motion_logic=motion_logic,
            angles=[0.0, 0.0, 0.0]
        )
    except Exception as e:
        print(f"解析人物记录时发生错误: {str(e)}\n记录内容: {line}")
        return None


# --- Tkinter UI Application ---
class RflySimControllerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("RflySim 模型控制器")
        self.root.geometry("800x700")

        # 添加点击日志监听相关变量
        self.last_click_log_size = 0
        self.last_click_position = None
        self.click_log_check_interval = 100  # 检查间隔（毫秒）
        self.is_click_log_monitoring = False

        # UE4 API and MapServe instances
        try:
            self.ue_api = UE4CtrlAPI.UE4CtrlAPI()
            self.map_server_api = UEMapServe.UEMapServe(DEFAULT_MAP_NAME)
            self.log_message("UE4CtrlAPI 和 UEMapServe 初始化成功。")
        except Exception as e:
            self.log_message(f"错误: UE4CtrlAPI/UEMapServe 初始化失败: {e}. 部分功能将受限。")
            self.ue_api = UE4CtrlAPI.UE4CtrlAPI() if 'MockUE4CtrlAPI' not in str(UE4CtrlAPI) else UE4CtrlAPI()
            self.map_server_api = UEMapServe.UEMapServe(DEFAULT_MAP_NAME) if 'MockUEMapServe' not in str(UEMapServe) else UEMapServe()


        # Style
        self.style = ttk.Style()
        self.style.theme_use('clam') # Or 'alt', 'default', 'classic'

        # Notebook for tabs
        self.notebook = ttk.Notebook(root)
        
        # Tab 1: Redeploy
        self.tab_redeploy = ttk.Frame(self.notebook)
        self.notebook.add(self.tab_redeploy, text='场景恢复')
        self.create_redeploy_tab()

        # Tab 2: Map
        self.tab_map = ttk.Frame(self.notebook)
        self.notebook.add(self.tab_map, text='地图')
        self.create_map_tab()

        # Tab 3: Weather
        self.tab_weather = ttk.Frame(self.notebook)
        self.notebook.add(self.tab_weather, text='天候')
        self.create_weather_tab()

        # Tab 4: Model Generation
        self.tab_model_gen = ttk.Frame(self.notebook)
        self.notebook.add(self.tab_model_gen, text='静态模型生成')
        self.create_model_gen_tab()

        # Tab 5: Person Generation
        self.tab_person_gen = ttk.Frame(self.notebook)
        self.notebook.add(self.tab_person_gen, text='动态人物生成')
        self.create_person_gen_tab()

        # Tab 6: Settings
        self.tab_settings = ttk.Frame(self.notebook)
        self.notebook.add(self.tab_settings, text='设置')
        self.create_settings_tab()
        
        self.notebook.pack(expand=True, fill='both', padx=10, pady=5)

        # Log area
        log_frame = ttk.LabelFrame(root, text="日志和状态")
        log_frame.pack(fill='both', expand=True, padx=10, pady=5)
        self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=15, state='disabled')
        self.log_text.pack(fill='both', expand=True, padx=5, pady=5)
        
        # General Actions Frame
        general_actions_frame = ttk.Frame(root)
        general_actions_frame.pack(fill=tk.X, padx=10, pady=5)
        
        self.reset_id_button = ttk.Button(general_actions_frame, text="重置ID计数器", command=self.confirm_reset_id_counter)
        self.reset_id_button.pack(side=tk.LEFT, padx=5)

        self.stop_motion_button = ttk.Button(general_actions_frame, text="停止所有人物运动", command=self.handle_stop_all_motions)
        self.stop_motion_button.pack(side=tk.LEFT, padx=5)
        
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

        # 启动点击日志监听
        self.start_click_log_monitoring()

        # 在RflySimControllerApp.__init__中初始化
        self.character_motion_manager = CharacterMotionManager(self.log_message)
        self.character_motion_manager.start()

        restore_scene_from_id_counter(self)

    def log_message(self, message):
        if not hasattr(self, 'log_text') or not self.log_text.winfo_exists(): return
        self.log_text.config(state='normal')
        # 支持多行日志自动分行
        for line in message.replace('\\n', '\n').split('\n'):
            if line.strip():
                self.log_text.insert(tk.END, f"{time.strftime('%H:%M:%S')} - {line}\n")
        self.log_text.see(tk.END)
        self.log_text.config(state='disabled')
        print(f"UI LOG: {message}")

    def on_closing(self):
        if hasattr(self, '_is_closing') and self._is_closing:
            return
        self._is_closing = True
        try:
            self.log_message("正在关闭应用程序...")
            self.stop_click_log_monitoring()
            # 只在"同步关闭后台cmd"时停止人物运动
            if hasattr(self, 'close_mode_var') and self.close_mode_var.get() == "exit":
                self.character_motion_manager.remove_all()
                self.log_message("所有人物运动已请求停止。")
        except Exception as e:
            self.log_message(f"关闭时发生异常: {e}")
        finally:
            try:
                self.root.destroy()
            except Exception:
                pass
            # 根据设置决定是否强制退出
            if hasattr(self, 'close_mode_var') and self.close_mode_var.get() == "exit":
                import os
                os._exit(0)

    # --- Model Generation Tab ---
    def create_model_gen_tab(self):
        frame = self.tab_model_gen
        
        ttk.Label(frame, text="选择模型类型:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        self.model_type_var = tk.StringVar()
        model_names = [f"{v['name']} (ID: {v['classid']})" for k, v in MODEL_TYPES.items()]
        self.model_type_combo = ttk.Combobox(frame, textvariable=self.model_type_var, values=model_names, state="readonly", width=30)
        if model_names: self.model_type_combo.current(0)
        self.model_type_combo.grid(row=0, column=1, padx=5, pady=5, sticky=tk.EW)

        self.generate_static_model_button = ttk.Button(frame, text="在最后点击位置生成模型", command=self.handle_generate_static_model)
        self.generate_static_model_button.grid(row=1, column=0, columnspan=2, padx=5, pady=10)

    def handle_generate_static_model(self):
        self.log_message("尝试生成静态模型...")
        selected_model_str = self.model_type_var.get()
        if not selected_model_str:
            messagebox.showerror("错误", "请选择一个模型类型。")
            self.log_message("静态模型生成失败：未选择模型类型。")
            return

        model_classid = None
        for k,v in MODEL_TYPES.items():
            if f"{v['name']} (ID: {v['classid']})" == selected_model_str:
                model_classid = v['classid']
                break
        
        if model_classid is None:
            messagebox.showerror("错误", "无效的模型类型选择。")
            self.log_message(f"静态模型生成失败：无效选择 {selected_model_str}。")
            return

        err_msg, position = get_last_click_position_from_log()
        if err_msg:
            messagebox.showerror("获取位置失败", err_msg)
            self.log_message(f"静态模型生成失败：{err_msg}")
            return
        if position is None: # Should be caught by err_msg, but as a safeguard
            messagebox.showerror("获取位置失败", "未能获取点击位置。")
            self.log_message("静态模型生成失败：未能获取点击位置。")
            return

        self.log_message(f"获取到点击位置: {position}，模型类型ClassID: {model_classid}")
        
        result_msg, model_id = create_static_model(model_classid, position, self.ue_api, self)
        self.log_message(result_msg)
        if model_id is None:
            messagebox.showerror("生成失败", result_msg)
        else:
            pass

    # --- Person Generation Tab ---
    def create_person_gen_tab(self):
        frame = self.tab_person_gen
        
        # 创建主框架
        main_frame = ttk.Frame(frame)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
        
        # 角色选择部分
        char_frame = ttk.LabelFrame(main_frame, text="角色选择")
        char_frame.pack(fill=tk.X, padx=5, pady=5)
        
        ttk.Label(char_frame, text="选择人物角色:").pack(side=tk.LEFT, padx=5, pady=5)
        self.person_char_var = tk.StringVar()
        char_names = [v['name'] for k, v in CHARACTERS.items()]
        self.person_char_combo = ttk.Combobox(char_frame, textvariable=self.person_char_var, values=char_names, state="readonly", width=20)
        if char_names: self.person_char_combo.current(0)
        self.person_char_combo.pack(side=tk.LEFT, padx=5, pady=5)

        # 巡逻点模式选择部分
        mode_frame = ttk.LabelFrame(main_frame, text="巡逻点模式")
        mode_frame.pack(fill=tk.X, padx=5, pady=5)
        
        self.patrol_mode_var = tk.StringVar(value="random")
        ttk.Radiobutton(mode_frame, text="随机生成巡逻点", variable=self.patrol_mode_var, 
                       value="random", command=self.on_patrol_mode_change).pack(side=tk.LEFT, padx=5, pady=5)
        ttk.Radiobutton(mode_frame, text="从地图点击获取巡逻点", variable=self.patrol_mode_var, 
                       value="click", command=self.on_patrol_mode_change).pack(side=tk.LEFT, padx=5, pady=5)

        # 随机模式框架
        self.random_frame = ttk.LabelFrame(main_frame, text="随机巡逻点设置")
        self.random_frame.pack(fill=tk.X, padx=5, pady=5)
        
        ttk.Label(self.random_frame, text="巡逻点数量:").pack(side=tk.LEFT, padx=5, pady=5)
        self.num_patrol_points_var = tk.StringVar(value="5")
        self.num_patrol_points_entry = ttk.Entry(self.random_frame, textvariable=self.num_patrol_points_var, width=10)
        self.num_patrol_points_entry.pack(side=tk.LEFT, padx=5, pady=5)

        # 点击模式框架
        self.click_frame = ttk.LabelFrame(main_frame, text="手动设置巡逻点")
        self.click_frame.pack(fill=tk.X, padx=5, pady=5)
        
        # 巡逻点列表显示
        points_frame = ttk.Frame(self.click_frame)
        points_frame.pack(fill=tk.X, padx=5, pady=5)
        
        ttk.Label(points_frame, text="已获取的巡逻点:").pack(anchor=tk.W)
        self.patrol_points_text = scrolledtext.ScrolledText(points_frame, height=5, width=50)
        self.patrol_points_text.pack(fill=tk.X, pady=5)
        self.patrol_points_text.config(state='disabled')
        
        # 按钮框架
        button_frame = ttk.Frame(self.click_frame)
        button_frame.pack(fill=tk.X, padx=5, pady=5)
        
        self.get_patrol_point_button = ttk.Button(button_frame, 
            text="自动获取点击位置作为巡逻点", 
            command=self.toggle_click_log_monitoring)
        self.get_patrol_point_button.pack(side=tk.LEFT, padx=5, pady=5)
        
        # 添加状态标签
        self.monitoring_status_label = ttk.Label(button_frame, text="监听状态: 已停止")
        self.monitoring_status_label.pack(side=tk.LEFT, padx=5, pady=5)

        # 清除按钮框架
        clear_frame = ttk.Frame(self.click_frame)
        clear_frame.pack(fill=tk.X, padx=5, pady=5)
        
        self.clear_patrol_points_button = ttk.Button(clear_frame, text="清除所有巡逻点", 
                                                   command=self.handle_clear_patrol_points)
        self.clear_patrol_points_button.pack(side=tk.LEFT, padx=5, pady=5)

        # 生成按钮框架
        generate_frame = ttk.Frame(main_frame)
        generate_frame.pack(fill=tk.X, padx=5, pady=10)
        
        self.generate_person_button = ttk.Button(generate_frame, text="生成人物", 
                                               command=self.handle_generate_person)
        self.generate_person_button.pack(pady=5)

        # 存储巡逻点列表
        self.patrol_points = []
        
        # 初始化UI状态
        self.on_patrol_mode_change()

    def on_patrol_mode_change(self):
        """处理巡逻点模式切换"""
        if self.patrol_mode_var.get() == "random":
            self.random_frame.pack(fill=tk.X, padx=5, pady=5)
            self.click_frame.pack_forget()
        else:
            self.random_frame.pack_forget()
            self.click_frame.pack(fill=tk.X, padx=5, pady=5)

    def toggle_click_log_monitoring(self):
        """切换点击日志监听状态"""
        if self.is_click_log_monitoring:
            self.stop_click_log_monitoring()
            self.get_patrol_point_button.config(text="开始自动获取点击位置")
            self.monitoring_status_label.config(text="监听状态: 已停止")
        else:
            self.start_click_log_monitoring()
            self.get_patrol_point_button.config(text="停止自动获取点击位置")
            self.monitoring_status_label.config(text="监听状态: 正在监听")
            self.log_message("已开始自动获取点击位置，请在地图上点击以添加巡逻点")

    def handle_get_patrol_point(self):
        """获取当前点击位置作为巡逻点"""
        if not self.last_click_position:
            messagebox.showinfo("提示", "请先在地图上点击位置")
            return

        self.patrol_points.append(self.last_click_position)
        self.update_patrol_points_display()
        self.log_message(f"已添加巡逻点: [{self.last_click_position[0]:.2f}, {self.last_click_position[1]:.2f}, {self.last_click_position[2]:.2f}]")

    def handle_clear_patrol_points(self):
        self.patrol_points = []
        self.update_patrol_points_display()
        self.log_message("已清除所有巡逻点")

    def update_patrol_points_display(self):
        """更新巡逻点列表显示"""
        self.patrol_points_text.config(state='normal')
        self.patrol_points_text.delete(1.0, tk.END)
        for i, point in enumerate(self.patrol_points, 1):
            self.patrol_points_text.insert(tk.END, 
                f"点 {i}: [{point[0]:.2f}, {point[1]:.2f}, {point[2]:.2f}]\n")
        self.patrol_points_text.config(state='disabled')

    def handle_generate_person(self):
        """处理人物生成"""
        self.log_message("尝试生成人物模型...")
        
        # 检查角色选择
        selected_char_str = self.person_char_var.get()
        if not selected_char_str:
            messagebox.showerror("错误", "请选择一个人物角色。")
            self.log_message("人物生成失败：未选择角色。")
            return

        # 获取角色信息
        char_details_key = None
        for k,v in CHARACTERS.items():
            if v['name'] == selected_char_str:
                char_details_key = k
                break
        
        if char_details_key is None:
            messagebox.showerror("错误", "无效的人物角色选择。")
            self.log_message(f"人物生成失败：无效选择 {selected_char_str}。")
            return
        
        char_to_spawn = CHARACTERS[char_details_key]

        # 获取起始位置
        err_msg, position = get_last_click_position_from_log()
        if err_msg:
            messagebox.showerror("获取位置失败", err_msg)
            self.log_message(f"人物生成失败：{err_msg}")
            return
        if position is None:
            messagebox.showerror("获取位置失败", "未能获取点击位置。")
            self.log_message("人物生成失败：未能获取点击位置。")
            return

        # 根据模式获取巡逻点
        if self.patrol_mode_var.get() == "random":
            try:
                num_points = int(self.num_patrol_points_var.get())
                if not (1 <= num_points <= 20):
                    messagebox.showerror("错误", "巡逻点数量必须在 1 到 20 之间。")
                    self.log_message("人物生成失败：巡逻点数量无效。")
                    return
            except ValueError:
                messagebox.showerror("错误", "巡逻点数量必须是有效的数字。")
                self.log_message("人物生成失败：巡逻点数量非数字。")
                return
            
            patrol_points = generate_random_patrol_points(num_points, position, map_server_api=self.map_server_api)
        else:  # click mode
            if not self.patrol_points:
                messagebox.showerror("错误", "请先添加至少一个巡逻点。")
                self.log_message("人物生成失败：未添加巡逻点。")
                return
            
            patrol_points = [position] + self.patrol_points  # 使用当前位置作为起始点

        if not patrol_points:
            msg = "未能生成巡逻路径点。"
            messagebox.showerror("错误", msg)
            self.log_message(f"人物生成失败: {msg}")
            return
            
        self.log_message(f"为 {char_to_spawn['name']} 生成了 {len(patrol_points)} 个巡逻点")
        for i, point in enumerate(patrol_points):
            self.log_message(f"巡逻点 {i+1}: [{point[0]:.2f}, {point[1]:.2f}, {point[2]:.2f}]")

        result_msg, person_id, motion_logic = create_person_model(char_to_spawn, position, patrol_points, self.ue_api, self)
        self.log_message(result_msg)

        if person_id is not None:
            if self.patrol_mode_var.get() == "click":
                self.handle_clear_patrol_points()
            
            # 添加人物到调度器
            char_info_obj = CharacterInfo(
                model_id=person_id,
                name=char_to_spawn['name'],
                walking_type=char_to_spawn['walking_id'] * 100000 + 30,
                standing_type=char_to_spawn['standing_id'] * 100000 + 30,
                position=position,
                patrol_points=patrol_points,
                motion_logic=motion_logic,
                angles=[0.0, 0.0, 0.0]
            )
            self.character_motion_manager.add_character(char_info_obj)
        else:
            messagebox.showerror("生成失败", result_msg)


    # --- Redeploy Tab ---
    def create_redeploy_tab(self):
        frame = self.tab_redeploy
        self.redeploy_button = ttk.Button(frame, text="重新部署所有记录的模型和人物", command=self.handle_redeploy)
        self.redeploy_button.pack(padx=20, pady=20)

    def handle_redeploy(self):
        self.log_message("开始重新部署所有模型...")
        if messagebox.askyesno("确认", "这将停止当前所有人物运动并从文件重新部署。是否继续?"):
            # 停止所有人物运动
            self.character_motion_manager.remove_all()
            # 重新加载所有模型和人物
            static_models = []
            character_models_info = []
            if not os.path.exists(ID_COUNTER_FILE):
                self.log_message(f"错误：找不到ID计数器文件: {ID_COUNTER_FILE}")
                return
            with open(ID_COUNTER_FILE, 'r') as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    model = parse_model_line(line)
                    if model:
                        static_models.append(model)
                        continue
                    character = parse_character_line(line)
                    if character:
                        character_models_info.append(character)
            self.log_message(f"找到 {len(static_models)} 个静态模型和 {len(character_models_info)} 个人物模型。")
            # 部署静态模型
            for model in static_models:
                try:
                    self.ue_api.sendUE4Pos(model.model_id, model.model_type, 0, model.position, model.angles)
                    self.log_message(f"已部署静态模型 ID: {model.model_id}, 类型: {model.model_type} @ {model.position}")
                except Exception as e:
                    self.log_message(f"部署静态模型 {model.model_id} 失败: {str(e)}")
            # 部署人物并加入调度器
            for char_info in character_models_info:
                try:
                    self.ue_api.sendUE4Pos(char_info.model_id, char_info.walking_type, 0, char_info.position, char_info.angles or [0,0,0])
                    self.log_message(f"已部署人物 {char_info.name} (ID: {char_info.model_id}) @ {char_info.position}")
                    if char_info.patrol_points and char_info.motion_logic:
                        self.character_motion_manager.add_character(char_info)
                    else:
                        self.log_message(f"人物 {char_info.name} (ID: {char_info.model_id}) 无巡逻点或运动逻辑，仅放置。")
                except Exception as e:
                    self.log_message(f"部署或启动人物 {char_info.name} (ID: {char_info.model_id}) 失败: {str(e)}")
            self.log_message("重新部署完成。")
            messagebox.showinfo("完成", "重新部署操作已执行。查看日志获取详情。")
        else:
            self.log_message("重新部署操作已取消。")

    # --- General Actions ---
    def confirm_reset_id_counter(self):
        if messagebox.askyesno("确认重置", "这将清空 'model_id_counter.txt' 文件。\n所有已记录的ID将被擦除。此操作无法撤销。\n是否确定要重置ID计数器?"):
            self.log_message("正在重置ID计数器...")
            result = reset_id_counter_file()
            self.log_message(result)
            messagebox.showinfo("ID计数器", result)

    def handle_stop_all_motions(self):
        self.log_message("请求停止所有人物运动...")
        self.character_motion_manager.remove_all()
        self.log_message("所有人物运动已停止（或已发送停止请求）。")
        messagebox.showinfo("运动控制", "已发送停止所有人物运动的请求。")

    def start_click_log_monitoring(self):
        """启动点击日志监听"""
        if not self.is_click_log_monitoring:
            self.is_click_log_monitoring = True
            self.check_click_log()
            self.log_message("已启动点击日志监听")

    def stop_click_log_monitoring(self):
        """停止点击日志监听"""
        self.is_click_log_monitoring = False
        self.log_message("已停止点击日志监听")

    def check_click_log(self):
        """检查点击日志文件变化"""
        if not self.is_click_log_monitoring:
            return

        try:
            if not os.path.exists(CLICK_LOG_PATH):
                self.root.after(self.click_log_check_interval, self.check_click_log)
                return

            current_size = os.path.getsize(CLICK_LOG_PATH)
            if current_size != self.last_click_log_size:
                self.last_click_log_size = current_size
                err_msg, position = get_last_click_position_from_log()
                if position is not None and position != self.last_click_position:
                    self.last_click_position = position
                    self.log_message(f"检测到新的点击位置: [{position[0]:.2f}, {position[1]:.2f}, {position[2]:.2f}]")
                    # 如果当前在点击模式下，自动添加巡逻点
                    if hasattr(self, 'patrol_mode_var') and self.patrol_mode_var.get() == "click":
                        self.handle_get_patrol_point()

        except Exception as e:
            self.log_message(f"检查点击日志时发生错误: {str(e)}")

        # 继续监听
        self.root.after(self.click_log_check_interval, self.check_click_log)

    def create_weather_tab(self):
        frame = self.tab_weather
        ttk.Label(frame, text="选择天气类型:").pack(pady=10)
        self.weather_var = tk.IntVar(value=0)
        weather_names = [f"{v} ({k})" for k, v in WEATHER_TYPES.items()]
        self.weather_combo = ttk.Combobox(frame, values=weather_names, state="readonly", width=20)
        self.weather_combo.current(0)
        self.weather_combo.pack(pady=5)

        # 新增天时输入
        time_frame = ttk.Frame(frame)
        time_frame.pack(pady=5)
        ttk.Label(time_frame, text="天时 (0-2400):").pack(side=tk.LEFT)
        self.weather_time_var = tk.StringVar(value="1200")
        self.weather_time_entry = ttk.Entry(time_frame, textvariable=self.weather_time_var, width=8)
        self.weather_time_entry.pack(side=tk.LEFT, padx=5)

        def on_set_weather():
            idx = self.weather_combo.current()
            weather_id = list(WEATHER_TYPES.keys())[idx]
            try:
                # 获取天时
                try:
                    weather_time = int(self.weather_time_var.get())
                    if not (0 <= weather_time <= 2400):
                        raise ValueError
                except ValueError:
                    messagebox.showerror("输入错误", "天时必须为0-2400之间的整数")
                    return
                # 先创建天气控制器
                self.ue_api.sendUE4PosNew(100,804,[0,0,-8],[0,0,0],[0,0,0],[0,0,0,0,0,0,0,0])
                time.sleep(0.5)  # 稍作等待，确保组件创建
                # 切换天气
                self.ue_api.sendUE4ExtAct(100, [weather_id, weather_time] + [0]*14)
                self.log_message(f"已切换天气为：{WEATHER_TYPES[weather_id]}，天时：{weather_time}")
            except Exception as e:
                self.log_message(f"切换天气失败: {e}")

        set_btn = ttk.Button(frame, text="切换天候", command=on_set_weather)
        set_btn.pack(pady=10)

    def create_map_tab(self):
        frame = self.tab_map
        ttk.Label(frame, text="选择地图:").pack(pady=10)
        # 显示中文，实际命令用英文
        self.map_display_names = ["草地", "岛礁", "梅岭"]
        self.map_cmd_names = ["Grasslands", "island", "meiling"]
        self.map_combo = ttk.Combobox(frame, values=self.map_display_names, state="readonly", width=20)
        self.map_combo.current(0)
        self.map_combo.pack(pady=5)

        def on_switch_map():
            idx = self.map_combo.current()
            map_cmd = self.map_cmd_names[idx]
            map_display = self.map_display_names[idx]
            try:
                self.ue_api.sendUE4Cmd(f'RflyChangeMapbyName {map_cmd}')
                self.log_message(f"已切换地图为：{map_display}")
            except Exception as e:
                self.log_message(f"切换地图失败: {e}")

        set_btn = ttk.Button(frame, text="切换地图", command=on_switch_map)
        set_btn.pack(pady=10)

    def create_settings_tab(self):
        frame = self.tab_settings
        ttk.Label(frame, text="关闭UI窗口时cmd行为:").pack(pady=10)
        self.close_mode_var = tk.StringVar(value="silent")  # 默认静默

        ttk.Radiobutton(frame, text="关闭UI后后台cmd转为静默运行（推荐）", variable=self.close_mode_var, value="silent").pack(anchor=tk.W, padx=20, pady=5)
        ttk.Radiobutton(frame, text="关闭UI后同步关闭后台cmd", variable=self.close_mode_var, value="exit").pack(anchor=tk.W, padx=20, pady=5)

def is_ue_window_alive():
    """检测RflySim3D或UE窗口进程是否存在"""
    for proc in psutil.process_iter(['name']):
        try:
            if proc.info['name'] and ('RflySim3D' in proc.info['name'] or 'UE4' in proc.info['name']):
                return True
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            continue
    return False

def restore_scene_from_id_counter(app):
    if not os.path.exists(ID_COUNTER_FILE):
        return
    with open(ID_COUNTER_FILE, 'r') as f:
        first_line = f.readline().strip()
    map_name, weather, time_val = 'Grasslands', '晴天', '1200'
    m = re.search(r'MapName:(\w+)', first_line)
    if m: map_name = m.group(1)
    m = re.search(r'Weather:([^,]+)', first_line)
    if m: weather = m.group(1)
    m = re.search(r'Time:(\d+)', first_line)
    if m: time_val = m.group(1)
    # 切换地图
    if hasattr(app, 'map_combo') and hasattr(app, 'map_cmd_names'):
        if map_name in app.map_cmd_names:
            idx = app.map_cmd_names.index(map_name)
            app.map_combo.current(idx)
            app.ue_api.sendUE4Cmd(f'RflyChangeMapbyName {map_name}')
    # 切换天气
    if hasattr(app, 'weather_combo') and hasattr(app, 'weather_time_var'):
        weather_id = 0
        for k, v in WEATHER_TYPES.items():
            if v == weather:
                weather_id = k
                break
        app.weather_combo.current(weather_id)
        app.weather_time_var.set(str(time_val))
        app.ue_api.sendUE4PosNew(100,804,[0,0,-8],[0,0,0],[0,0,0],[0,0,0,0,0,0,0,0])
        time.sleep(0.5)
        app.ue_api.sendUE4ExtAct(100, [weather_id, int(time_val)] + [0]*14)

if __name__ == '__main__':
    app_root = tk.Tk()
    controller_app = RflySimControllerApp(app_root)
    # Ensure mock objects are used if imports failed at the top level
    if 'MockUE4CtrlAPI' in str(UE4CtrlAPI):
         controller_app.log_message("警告: 应用程序正在以模拟模式运行UE4连接。")
    app_root.mainloop() 